Skip to content

ci: OpenAPI contract validation (#37 phase 1)#44

Merged
linfangw merged 4 commits into
mainfrom
feat/issue-37-openapi-contract-checks
May 18, 2026
Merged

ci: OpenAPI contract validation (#37 phase 1)#44
linfangw merged 4 commits into
mainfrom
feat/issue-37-openapi-contract-checks

Conversation

@linfangw
Copy link
Copy Markdown
Contributor

Implements Phase 1 of #37.

Summary

  • scripts/validate-openapi-contract.mjs — zero-dep check on the website-mirrored docs/openapi/qveris-public-api.openapi.json: file exists + valid JSON, info.version present, core paths present (/search, /tools/by-ids, /tools/execute, /auth/usage/history/v2, /auth/credits/ledger), and the component schemas the toolkit deserializes (search/execute/usage/ledger families).
  • scripts/test-openapi-contract.mjs — regression tests (valid spec passes; missing file / bad JSON / missing version / missing path / missing schema all fail).
  • .github/workflows/openapi-contract.yml — runs both, path-scoped to docs/openapi/** + the scripts + the workflow, mirroring ecosystem-validate.yml.

Scope / non-goals

Phase 1 only. Type generation for MCP (openapi-typescript) and Python SDK (datamodel-code-generator) — Phases 2–3 — are intentionally deferred to separate PRs per the issue's "small PRs by phase" guidance. No package/runtime changes; MCP, CLI, and Python SDK public APIs are untouched and backward compatible.

Test plan

  • node scripts/validate-openapi-contract.mjs → OK against the checked-in spec
  • node scripts/test-openapi-contract.mjs → 6/6 regression tests pass
  • Workflow YAML parses; triggers scoped so it does not run on unrelated changes

Follow-ups (separate issues/PRs): Phase 2 (generated TS/Python artifacts + git diff --exit-code CI), Phase 3 (gradual internal adoption).

Add a zero-dependency drift check for the website-mirrored public
OpenAPI spec (docs/openapi/qveris-public-api.openapi.json): asserts the
file exists and is valid JSON, info.version is present, the core agent
paths exist, and the response schemas the toolkit deserializes are
present. Mirrors the ecosystem-validate workflow pattern (validator +
regression test + path-scoped workflow).

This is phase 1 only — type generation for MCP/Python SDK (phases 2-3)
is intentionally deferred to separate PRs per the issue, so existing
MCP, CLI, and Python SDK public APIs are unchanged.

Closes #37 phase 1.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a zero-dependency OpenAPI contract validation script and a corresponding regression test suite to detect drift in the mirrored public API specification. The validator ensures that essential paths and component schemas required by the toolkit are present. Review feedback highlights the need for more portable path resolution using fileURLToPath to support Windows environments and recommends adding a check to verify that the parsed JSON is a valid object before accessing its properties.

Comment thread scripts/validate-openapi-contract.mjs Outdated
Comment thread scripts/test-openapi-contract.mjs Outdated
Comment thread scripts/validate-openapi-contract.mjs
- Use fileURLToPath(import.meta.url) for REPO_ROOT in both scripts
  (portable on Windows and paths with spaces).
- Guard that the parsed spec is a non-null object before accessing
  properties, so `null`/array JSON yields a clean validation error
  instead of a TypeError crash. Covered by a new regression test.
@linfangw
Copy link
Copy Markdown
Contributor Author

Addressed all three Gemini comments in 2d6dc5e:

  • [high] Portable path resolution (validator + test): switched new URL(import.meta.url).pathnamefileURLToPath(import.meta.url) in both scripts/validate-openapi-contract.mjs and scripts/test-openapi-contract.mjs.
  • [medium] Non-object JSON guard: added an explicit !spec || typeof spec !== 'object' || Array.isArray(spec) check after JSON.parse, so null/array specs fail with not a valid OpenAPI object instead of a TypeError. Added a regression test ("rejects valid JSON that is not an object") — 7/7 tests pass; validator still green on the real spec.

linfangw and others added 2 commits May 18, 2026 12:00
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
A suggestion applied via the GitHub UI (cd0fa09) re-inserted the
JSON.parse catch block, leaving an orphaned `} catch (error) {` that
crashed CI with `SyntaxError: Unexpected token 'catch'`. Remove the
duplicate; validator + 7 regression tests pass.
@linfangw
Copy link
Copy Markdown
Contributor Author

Fixed the failing Validate OpenAPI contract check in 80f8e74.

Root cause: commit cd0fa09 ("Update scripts/validate-openapi-contract.mjs", applied via the GitHub UI — looks like the Gemini object-guard suggestion was committed on top of code that already had the fix) duplicated the JSON.parse try/catch block, leaving an orphaned } catch (error) {SyntaxError: Unexpected token 'catch', so the script failed to even parse.

Removed the duplicated block (fix-forward, no force-push so cd0fa09 history is preserved). Validator runs clean and all 7 regression tests pass; CI is now green.

@linfangw linfangw merged commit 9f96026 into main May 18, 2026
1 check passed
linfangw added a commit that referenced this pull request May 18, 2026
…ase 2) (#48)

* feat(openapi): generate checked-in MCP/Python contract types (#37 phase 2)

Implements phase 2 of #37 / closes #46. Generated artifacts are a contract
reference; hand-written MCP/SDK public models are unchanged (no runtime or
package behavior change — backward compatible).

- MCP/TS: `openapi-typescript@7.4.4` (pinned devDep) -> checked-in
  packages/mcp/src/generated/openapi.d.ts; `npm run gen:openapi` regenerates.
- Python SDK: `datamodel-code-generator==0.26.3` (pinned dev extra) ->
  checked-in packages/python-sdk/qveris/generated/openapi_models.py
  (pydantic v2, py3.8 target) + package __init__ documenting regeneration.
- CI: .github/workflows/openapi-types.yml regenerates both with the pinned
  generators and fails on `git diff --exit-code`, path-scoped to
  docs/openapi/** + the generators (mirrors openapi-contract.yml from #44).

Verified: artifacts are byte-reproducible from the checked-in spec
(empty diff on regen), the .d.ts typechecks under tsc --strict, and the
Python models pass py_compile.

* feat(contract): CLI + Python OpenAPI contract guards (#37 phase 3) (#49)

* feat(contract): adopt generated OpenAPI types — CLI + Python guards (#37 phase 3)

Implements phase 3 of #37 / closes #47. First low-risk adoption step;
public MCP/CLI/Python SDK APIs unchanged (additive tests + CI only).

- CLI (JS/MJS): packages/cli/test/openapi-contract.test.mjs asserts every
  endpoint+method the CLI calls (kept in sync with src/client/api.mjs:
  POST /search, /tools/by-ids, /tools/execute; GET /auth/credits,
  /auth/usage/history/v2, /auth/credits/ledger) exists in the mirrored
  public OpenAPI spec — the issue's "CLI contract tests before type gen".
- Python SDK: tests/test_openapi_contract.py starts *consuming* the
  generated qveris.generated.openapi_models as a drift guard (core contract
  models present + pydantic + smoke roundtrip). Hand-written qveris.types
  stays the public surface; field alignment stays gradual by design.
- CI: .github/workflows/contract-tests.yml runs both on PRs (the existing
  cli/python *-publish workflows only fire on release tags, so without this
  the guards would not gate PRs).

Verified locally: CLI `node --test` 7/7 pass; Python pytest 11/11 pass.
Builds on #46/#48 (needs the generated artifacts) — merge after #48.

* fix(contract): load generated models standalone + robust shape check (#47)

CI python-contract failed: importing qveris.generated.openapi_models pulled
qveris/__init__.py (httpx not installed in the minimal contract job). Load the
generated file by path so the guard only needs pydantic. Replace the
instantiation smoke with a model_fields shape assertion (the generated file's
__future__ annotations + constrained types need model_rebuild() to validate;
the field map is the stable contract signal regardless).

* Update packages/mcp/package.json

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants